﻿Imports System.Runtime.Serialization
Imports System.IO
Imports System.Text

<Serializable()>
Public Class RawObject
    Implements ISerializable

#Region "Serialization"

    Private Sub New(info As SerializationInfo, context As StreamingContext)
        Dim version As Integer = info.GetInt32("version")
        'Dim options As Dictionary(Of String, String) = CType(context.Context, Dictionary(Of String, String))

        If version >= 1 Then
            mObjectType = CType(info.GetInt32("objectType"), ObjectType)
            mObjectSubType = CType(info.GetInt32("objectSubType"), ObjectSubType)
            mObjectId = info.GetString("objectId")
            ' If options Is Nothing OrElse Not options.ContainsKey("disconnected") OrElse options("disconnected") <> "yes" Then
            If CloneHelper.GetOption(context, "disconnected", "no") = "no" Then
                mLibrary = info.GetValue(Of RawLibrary)("library")
                mSource = info.GetValue(Of RawSource)("source")
            Else
                mLibrary = Nothing
                mSource = Nothing
            End If
            mTokens = info.GetValue(Of List(Of Token))("tokens")
        End If
    End Sub

    Public ReadOnly Property UniqueKey As String
        Get
            Return String.Format("{0}!{1}!{2}", ObjectType.ToString, ObjectSubType.ToString, ObjectId)
        End Get
    End Property



    Public Sub GetObjectData(info As SerializationInfo, context As StreamingContext) Implements ISerializable.GetObjectData
        info.AddValue("version", 1)
        'Dim options As Dictionary(Of String, String) = CType(context.Context, Dictionary(Of String, String))

        'Version 1 data
        info.AddValue("objectType", mObjectType)
        info.AddValue("objectSubType", mObjectSubType)
        info.AddValue("objectId", ObjectId)
        'If options Is Nothing OrElse Not options.ContainsKey("disconnected") OrElse options("disconnected") <> "yes" Then
        If CloneHelper.GetOption(context, "disconnected", "no") = "no" Then
            info.AddValue("library", mLibrary)
            info.AddValue("source", mSource)
        End If
        info.AddValue("tokens", Tokens)
    End Sub
#End Region

    Public Overrides Function ToString() As String
        Return "[" & mObjectSubType.ToString & ":" & mObjectId & "]"
    End Function

    Private mObjectId As String
    Public ReadOnly Property ObjectId() As String
        Get
            Return mObjectId
        End Get
    End Property

    Public Sub SetObjectId(objectId As String)
        mObjectId = objectId
    End Sub

    Private mTokens As List(Of Token)
    Public ReadOnly Property Tokens() As List(Of Token)
        Get
            Return mTokens
        End Get
    End Property

    Sub New(objectType As ObjectType, objectSubType As ObjectSubType)
        Me.New()

        mObjectType = objectType
        mObjectSubType = objectSubType
    End Sub

    Public Sub New(objectType As ObjectType, objectSubType As ObjectSubType, objectId As String, library As RawLibrary, source As RawSource)
        Me.New()

        Me.mObjectType = objectType
        Me.mObjectSubType = objectSubType
        Me.mObjectId = objectId
        Me.mLibrary = library
        Me.mSource = source
    End Sub

    Protected Sub New()
        mTokens = New List(Of Token)
    End Sub

    ''' <summary>
    ''' Returns a list of tokens from the object that match any of the requested token names
    ''' </summary>
    ''' <param name="tokenList"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetTokens(tokenList As IEnumerable(Of String)) As IEnumerable(Of Token)
        Return (From token As Token In mTokens Where tokenList.Contains(token.Name))
    End Function

    ''' <summary>
    ''' returns a list of tokens from the object that match the requested token name.
    ''' </summary>
    ''' <param name="tokenName"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetTokens(tokenName As String) As IEnumerable(Of Token)
        Return (From token As Token In mTokens Where token.Name = tokenName)
    End Function

    Public Function GetFirstToken(name As String) As Token
        'Return (From token As Token In tokens Where token.name = name).FirstOrDefault
        Return GetFirstToken(name, False)
    End Function

    Public Function GetFirstToken(name As String, followCopyTags As Boolean) As Token
        'check if the object as the token directly 
        Dim ret As Token = (From token As Token In Tokens Where token.Name = name).FirstOrDefault
        If ret IsNot Nothing Then
            Return ret
        End If
        If Not followCopyTags Then
            Return Nothing
        End If

        'check recursivly for COPY_TAGS_FROM token
        Dim copyToken As Token = GetFirstToken("COPY_TAGS_FROM")
        If copyToken IsNot Nothing Then
            Dim copy As RawObject = mLibrary.GetObjectByType(Me.mObjectType, copyToken.GetArgumentOrDefault(0, ""))
            If copy IsNot Nothing Then
                Return copy.GetFirstToken(name, True)
            End If
        End If
        Return Nothing
    End Function

    ''' <summary>
    ''' Returns the first token found from the list of token names passed to the function
    ''' </summary>
    ''' <param name="tokenList">A list of strings representing the names of tokens to look for</param>
    ''' <param name="followCopyTags">If True this function will search other objects that this object copies tags from.</param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetFirstTokenOf(tokenList As IEnumerable(Of String), followCopyTags As Boolean) As Token
        For Each tokenName As String In tokenList
            Dim token As Token = GetFirstToken(tokenName, followCopyTags)
            If token IsNot Nothing Then
                Return token
            End If
        Next
        Return Nothing
    End Function

    ''' <summary>
    ''' Returns all the tokens with the specified name
    ''' </summary>
    ''' <param name="name"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetAllTokens(name As String) As IEnumerable(Of Token)
        Return (From token As Token In Tokens Where token.Name = name)
    End Function

    ''' <summary>
    ''' Returns a list of the Nth argument from over token that matches 'tokenName'.
    ''' </summary>
    ''' <param name="tokenName">The name of the tokens to seatch for</param>
    ''' <param name="argumentIndex">The index of the argument to return</param>
    ''' <param name="defaultValue">The value to return if a matched token does not contain an Nth element</param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetAllTokenArguments(tokenName As String, argumentIndex As Integer, defaultValue As String) As IEnumerable(Of String)
        Return From token As Token In GetAllTokens(tokenName) Select token.GetArgumentOrDefault(argumentIndex, defaultValue)
    End Function

    Public Function GetTokenValueAsInteger(name As String) As Integer
        Return GetTokenValueAsInteger(name, True)
    End Function

    Public Function GetTokenValueAsInteger(name As String, followCopyTags As Boolean) As Integer
        Dim token As Token = GetFirstToken(name)
        If token IsNot Nothing Then
            If token.HasArguments Then
                Dim val As Integer = 0
                Integer.TryParse(token.Arguments(0), val)
                Return val
            Else
                Return 0
            End If
        Else
            If followCopyTags Then
                Dim copyToken As Token = GetFirstToken("USE_MATERIAL_TEMPLATE")
                If copyToken IsNot Nothing Then
                    Dim copy As RawObject = mLibrary.GetObjectByType(ObjectType.MATERIAL_TEMPLATE, copyToken.GetArgumentOrDefault(0, ""))
                    If copy IsNot Nothing Then
                        'Return copy.getFirstToken(name, True)
                        Return copy.GetTokenValueAsInteger(name, True)
                    End If
                End If
                Return 0
            Else
                Return 0
            End If
        End If
    End Function

    Private mObjectType As ObjectType
    Public ReadOnly Property ObjectType() As ObjectType
        Get
            Return mObjectType
        End Get
    End Property

    Private mObjectSubType As ObjectSubType
    Public ReadOnly Property ObjectSubType() As ObjectSubType
        Get
            Return mObjectSubType
        End Get
    End Property

    Private mLibrary As RawLibrary
    Public Property Library As RawLibrary
        Get
            Return mLibrary
        End Get
        Set(value As RawLibrary)
            mLibrary = value

        End Set
    End Property

    Private mSource As RawSource
    Public Property Source() As RawSource
        Get
            Return mSource
        End Get
        Set(value As RawSource)
            mSource = value
        End Set
    End Property

    Public Sub MoveToSource(targetSource As RawSource)
        If Me.Library IsNot Nothing AndAlso Me.mLibrary IsNot targetSource.Library Then
            Throw New ArgumentException("Objects can only be moved to sources within the same library.")
        End If
        mSource = targetSource
    End Sub

    Public Sub MoveToLibrary(library As RawLibrary)
        If mSource IsNot Nothing Then
            MoveToLibrary(library, mSource)
        Else
            Throw New ArgumentException("this overload requires that the object already be in a source.")
        End If
    End Sub

    Public Sub MoveToLibrary(targetLibrary As RawLibrary, targetSource As RawSource)
        If Not targetLibrary.Sources.Contains(targetSource) Then
            Throw New ArgumentException("targetSource must be in targetLibrary")
        End If
        If mLibrary.GetRawObjectById(mObjectId) IsNot Nothing Then
            Throw New ArgumentException("There is already an object with this objectId in the specified library.")
        End If
        If targetSource.ObjectType <> Me.mObjectType Then
            Throw New ArgumentException("The object is incompatable with the specified source.")
        End If

        If mLibrary IsNot Nothing Then
            'TODO: Remove the object from the library
        End If
        If mSource IsNot Nothing Then
            mSource = Nothing
        End If

        mSource = Source
        mLibrary = Library
        mLibrary.AddObject(Me)
    End Sub

    Public Function GetFirstTokenArguments(tokenName As String) As String
        Dim token As Token = Me.GetFirstToken(tokenName)
        Return token.ArgumentsAsString
    End Function

    Public Function GetTokenArguments(tokenName As String, followCopyTags As Boolean) As System.Collections.Generic.IEnumerable(Of String)
        Dim ret As New List(Of String)
        For Each token As Token In Me.Tokens
            If token.Name = tokenName Then
                ret.Add(token.ArgumentsAsString)
            End If
        Next
        If followCopyTags Then
            'check recursivly for COPY_TAGS_FROM token
            Dim copyToken As Token = GetFirstToken("COPY_TAGS_FROM")
            If copyToken IsNot Nothing Then
                Dim copy As RawObject = mLibrary.GetObjectByType(Me.mObjectType, copyToken.Arguments(0))
                If copy IsNot Nothing Then
                    ret.AddRange(copy.GetTokenArguments(tokenName, True))
                End If
            End If
        End If
        Return ret
    End Function

    Public Sub WriteToStream(writer As TextWriter)
        writer.WriteLine(String.Format("[{0}:{1}]", mObjectSubType, mObjectId))
        Dim previousToken As Token = Nothing
        For Each token As Token In mTokens

            If previousToken IsNot Nothing Then
                Dim def As TokenDefinition = TokenDictionary.Instance.GetDefinition(token.Name)
                If def Is Nothing OrElse Not def.SticksTo.Contains(previousToken.Name) Then
                    writer.WriteLine()
                    writer.Write(vbTab)
                End If
            Else
                writer.Write(vbTab) 'first token is always on a new line
            End If

            writer.Write(token.ToString)
            previousToken = token
        Next
    End Sub

    Public Function WriteToString() As String
        Dim ret As New StringBuilder

        Dim writer As New StringWriter(ret)
        WriteToStream(writer)
        writer.Close()
        Return ret.ToString
    End Function

    Public Function GetDetachedCopy() As RawObject
        Dim ret As New RawObject
        ret.mObjectId = Me.mObjectId
        ret.mObjectType = Me.mObjectType
        ret.mObjectSubType = Me.mObjectSubType
        ret.mSource = Nothing
        ret.mLibrary = Nothing

        For Each Token As Token In mTokens
            ret.mTokens.Add(Token.GetCopy)
        Next

        Return ret
    End Function

    ''' <summary>
    ''' Constructs a rawObject from a string
    ''' </summary>
    ''' <param name="text">A portion of text that represents a rawObject</param>
    ''' <param name="library">The library to place the rawObject in</param>
    ''' <returns>the constructed rawObject</returns>
    ''' <remarks></remarks>
    Public Shared Function FromRawText(text As String, library As RawLibrary) As RawObject
        Dim rawReader As RawReader = rawReader.FromString(text)

        If rawReader.ReadNextToken Then
            Dim idToken As Token = rawReader.CurrentToken

            Dim subType As ObjectSubType
            If [Enum].TryParse(idToken.Name, subType) Then
                Dim objectType As ObjectType = library.InferTypeFromSubType(subType)
                If objectType <> objectType.ALL Then
                    Dim ret As New RawObject(objectType, subType, idToken.Arguments(0), library, Nothing)
                    Dim desc As RawObjectDescriptor = library.GetDescriptorFromTypeName(objectType.ToString)
                    ' Dim dummy As ObjectSubType

                    Do While rawReader.ReadNextToken
                        subType = ObjectSubType.ALL
                        [Enum].TryParse(rawReader.CurrentToken.Name, subType)
                        If Not desc.ObjectClasses.Contains(subType) Then
                            ret.Tokens.Add(rawReader.CurrentToken)
                        End If
                    Loop
                    Return ret
                Else
                    Return Nothing
                End If
            Else
                Return Nothing
            End If
        Else
            Return Nothing
        End If
    End Function

    Public ReadOnly Property TypeString As String
        Get
            Return mObjectType.ToString & ":" & mObjectSubType.ToString
        End Get
    End Property

    ''' <summary>
    ''' Removes all tokens with the specified name
    ''' </summary>
    ''' <param name="tokenName"></param>
    ''' <remarks></remarks>
    Public Sub RemoveToken(tokenName As String)
        For index As Integer = mTokens.Count - 1 To 0 Step -1
            If mTokens(index).Name = tokenName Then
                mTokens.RemoveAt(index)
            End If
        Next
    End Sub

    ''' <summary>
    ''' Inserts a token at the specified index
    ''' </summary>
    ''' <param name="tokenName">the token to insert</param>
    ''' <param name="arguments">the token's arguments</param>
    ''' <param name="index">the index in teh object to insert to tokens</param>
    ''' <remarks></remarks>
    Public Sub AddToken(tokenName As String, arguments As String, index As Integer)
        Dim token As New Token
        token.Name = tokenName
        token.ReparseArgs(arguments, Me)
        mTokens.Insert(index, token)
    End Sub

    ''' <summary>
    ''' Adds a new token with no argumenets
    ''' </summary>
    ''' <param name="tokenName"></param>
    ''' <remarks></remarks>
    Public Sub AddToken(tokenName As String)
        AddToken(tokenName, "")
    End Sub

    ''' <summary>
    ''' Adds a new token with the specified argument string
    ''' </summary>
    ''' <param name="tokenName"></param>
    ''' <param name="arguments"></param>
    ''' <remarks></remarks>
    Public Sub AddToken(tokenName As String, arguments As String)
        AddToken(tokenName, arguments, mTokens.Count)
    End Sub

    Private Function GetTissueOrMaterial(type As String) As IEnumerable(Of String)
        Dim ret As New HashSet(Of String)

        'Get all the locally defined materials
        For Each args As String In Me.GetTokenArguments("USE_" & type & "_TEMPLATE", True)
            Dim parts() As String = args.Split(":"c)
            If parts.Count >= 1 Then
                If Not ret.Contains(parts(0)) Then
                    ret.Add(parts(0))
                End If
            End If
        Next
        'get all the materials from the body detail plans
        For Each args As String In Me.GetTokenArguments("BODY_DETAIL_PLAN", True)
            Dim localParts() As String = args.Split(":"c) 'arg0 has the objectid of the body detail plan to use
            If localParts.Count >= 0 Then
                Dim obj As RawObject = mLibrary.GetObjectById(localParts(0)) 'get the body detail plan
                If obj IsNot Nothing Then
                    'add any "add_material" materials to the list
                    For Each planArgs As String In obj.GetTokenArguments("ADD_" & type, True)
                        Dim planParts() As String = planArgs.Split(":"c)
                        If planParts.Count >= 1 AndAlso Not ret.Contains(planParts(0)) Then
                            ret.Add(planParts(0))
                        End If
                    Next
                End If
            End If
        Next

        Return ret
    End Function

    Public Function GetMaterials() As IEnumerable(Of String)
        Return GetTissueOrMaterial("MATERIAL")
    End Function

    Public Function GetTissues() As IEnumerable(Of String)
        Return GetTissueOrMaterial("TISSUE")
    End Function

    Public ReadOnly Property TypeCode As TypeCode
        Get
            Return New TypeCode(Me.mObjectType, Me.mObjectSubType)
        End Get
    End Property

    Public ReadOnly Property HasToken(tokenName As String, followCopyTags As Boolean) As Boolean
        Get
            For Each token As Token In Me.Tokens
                If token.Name = tokenName Then
                    Return True
                End If
            Next
            If followCopyTags Then
                Dim copyTags As New List(Of String)({"COPY_TAGS_FROM", "USE_MATERIAL_TEMPLATE", "USE_TISSUE_TEMPLATE"})

                'check recursivly for COPY_TAGS_FROM token
                For Each t As String In copyTags
                    Dim copyToken As Token = GetFirstToken(t)
                    If copyToken IsNot Nothing Then
                        Dim copy As RawObject = mLibrary.GetObjectById(copyToken.Arguments(0))
                        If copy IsNot Nothing Then
                            Dim result As Boolean = copy.HasToken(tokenName, followCopyTags)
                            If result Then
                                Return True
                            End If
                        End If
                    End If
                Next
            End If

            Return False
        End Get
    End Property

    Public ReadOnly Property HasToken(tokenName As String) As Boolean
        Get
            Return HasToken(tokenName, True)
        End Get
    End Property

    ''' <summary>
    ''' Returns the object that this object inherited the requested token from.
    ''' </summary>
    Public Function GetTokenSourceObject(tokenName As String) As RawObject
        For Each token As Token In Me.Tokens
            If token.Name = tokenName Then
                Return Me
            End If
        Next
        'check recursivly for COPY_TAGS_FROM token
        Dim copyToken As Token = GetFirstToken("COPY_TAGS_FROM")
        If copyToken IsNot Nothing Then
            Dim copy As RawObject = mLibrary.GetObjectByType(Me.mObjectType, copyToken.Arguments(0))
            If copy IsNot Nothing Then
                Return copy.GetTokenSourceObject(tokenName)
            End If
        End If
        Return Nothing
    End Function

    ''' <summary>
    ''' Returns a list of all objects in the same library as this object that reference this object.
    ''' </summary>
    Public Function GetDependancies() As List(Of RawObject)
        Dim copyTags As New List(Of String)({"COPY_TAGS_FROM", "USE_MATERIAL_TEMPLATE", "USE_TISSUE_TEMPLATE"})

        Dim ret As New HashSet(Of RawObject)

        If mLibrary IsNot Nothing Then
            For Each obj As RawObject In (From obj1 As RawObject In mLibrary.GetAllObjects Where obj1 IsNot Me)
                For Each Token As Token In obj.GetTokens(copyTags)
                    If Token.GetArgumentOrDefault(0, "") = Me.mObjectId Then
                        If Not ret.Contains(obj) Then
                            ret.Add(obj)
                        End If
                    End If
                Next
            Next
        End If

        Return ret.ToList
    End Function

    ''' <summary>
    ''' Adds the tokens from an object template to the current rawObject
    ''' </summary>
    ''' <param name="clearExistingTokens">True to clear any existing tokens before appling the template</param>
    ''' <remarks></remarks>
    Public Sub GetTokensFromTemplate(clearExistingTokens As Boolean)
        If clearExistingTokens Then
            mTokens.Clear()
        End If
        Try
            If IO.Directory.Exists("Templates") Then
                Dim templatePath As String = "Templates\" & Me.ObjectType.ToString & "-" & Me.ObjectSubType.ToString & ".txt"
                If IO.File.Exists(templatePath) Then
                    Dim contents As String = ""
                    Dim fileReader As StreamReader = Nothing
                    Try
                        fileReader = New StreamReader(templatePath)
                        contents = fileReader.ReadToEnd

                        'Replace text tokens
                        contents = contents.Replace("%%ObjectId%%", mObjectId)
                    Catch ex As Exception
                        Throw
                    Finally
                        If fileReader IsNot Nothing Then
                            fileReader.Close()
                            fileReader.Dispose()
                        End If
                    End Try

                    Dim reader As RawReader = RawReader.FromString(contents)
                    Do While reader.ReadNextToken
                        mTokens.Add(reader.CurrentToken)
                    Loop
                End If
            End If
        Catch ex As Exception
            'Do nothing if there was an error
        End Try
    End Sub
End Class
